/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.log4j.net;

import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.ErrorCode;
import org.apache.log4j.helpers.LogLog;

import java.util.Properties;
import javax.jms.TopicConnection;
import javax.jms.TopicConnectionFactory;
import javax.jms.Topic;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.jms.Session;
import javax.jms.ObjectMessage;
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;

/**
 * A simple appender that publishes events to a JMS Topic. The events are
 * serialized and transmitted as JMS message type {@link ObjectMessage}.
 * 
 * <p>
 * JMS {@link Topic topics} and {@link TopicConnectionFactory topic connection
 * factories} are administered objects that are retrieved using JNDI messaging
 * which in turn requires the retreival of a JNDI {@link Context}.
 * 
 * <p>
 * There are two common methods for retrieving a JNDI {@link Context}. If a file
 * resource named <em>jndi.properties</em> is available to the JNDI API, it will
 * use the information found therein to retrieve an initial JNDI context. To
 * obtain an initial context, your code will simply call:
 * 
 * <pre>
 * InitialContext jndiContext = new InitialContext();
 * </pre>
 * 
 * <p>
 * Calling the no-argument <code>InitialContext()</code> method will also work
 * from within Enterprise Java Beans (EJBs) because it is part of the EJB
 * contract for application servers to provide each bean an environment naming
 * context (ENC).
 * 
 * <p>
 * In the second approach, several predetermined properties are set and these
 * properties are passed to the <code>InitialContext</code> contructor to
 * connect to the naming service provider. For example, to connect to JBoss
 * naming service one would write:
 * 
 * <pre>
 * Properties env = new Properties();
 * env.put(Context.INITIAL_CONTEXT_FACTORY,
 * 		&quot;org.jnp.interfaces.NamingContextFactory&quot;);
 * env.put(Context.PROVIDER_URL, &quot;jnp://hostname:1099&quot;);
 * env.put(Context.URL_PKG_PREFIXES, &quot;org.jboss.naming:org.jnp.interfaces&quot;);
 * InitialContext jndiContext = new InitialContext(env);
 * </pre>
 * 
 * where <em>hostname</em> is the host where the JBoss applicaiton server is
 * running.
 * 
 * <p>
 * To connect to the the naming service of Weblogic application server one would
 * write:
 * 
 * <pre>
 * Properties env = new Properties();
 * env.put(Context.INITIAL_CONTEXT_FACTORY,
 * 		&quot;weblogic.jndi.WLInitialContextFactory&quot;);
 * env.put(Context.PROVIDER_URL, &quot;t3://localhost:7001&quot;);
 * InitialContext jndiContext = new InitialContext(env);
 * </pre>
 * 
 * <p>
 * Other JMS providers will obviously require different values.
 * 
 * The initial JNDI context can be obtained by calling the no-argument
 * <code>InitialContext()</code> method in EJBs. Only clients running in a
 * separate JVM need to be concerned about the <em>jndi.properties</em> file and
 * calling {@link InitialContext#InitialContext()} or alternatively correctly
 * setting the different properties before calling
 * {@link InitialContext#InitialContext(java.util.Hashtable)} method.
 * 
 * @author Ceki G&uuml;lc&uuml;
 */
public class JMSAppender extends AppenderSkeleton {

	String securityPrincipalName;
	String securityCredentials;
	String initialContextFactoryName;
	String urlPkgPrefixes;
	String providerURL;
	String topicBindingName;
	String tcfBindingName;
	String userName;
	String password;
	boolean locationInfo;

	TopicConnection topicConnection;
	TopicSession topicSession;
	TopicPublisher topicPublisher;

	public JMSAppender() {
	}

	/**
	 * The <b>TopicConnectionFactoryBindingName</b> option takes a string value.
	 * Its value will be used to lookup the appropriate
	 * <code>TopicConnectionFactory</code> from the JNDI context.
	 */
	public void setTopicConnectionFactoryBindingName(String tcfBindingName) {
		this.tcfBindingName = tcfBindingName;
	}

	/**
	 * Returns the value of the <b>TopicConnectionFactoryBindingName</b> option.
	 */
	public String getTopicConnectionFactoryBindingName() {
		return tcfBindingName;
	}

	/**
	 * The <b>TopicBindingName</b> option takes a string value. Its value will
	 * be used to lookup the appropriate <code>Topic</code> from the JNDI
	 * context.
	 */
	public void setTopicBindingName(String topicBindingName) {
		this.topicBindingName = topicBindingName;
	}

	/**
	 * Returns the value of the <b>TopicBindingName</b> option.
	 */
	public String getTopicBindingName() {
		return topicBindingName;
	}

	/**
	 * Returns value of the <b>LocationInfo</b> property which determines
	 * whether location (stack) info is sent to the remote subscriber.
	 */
	public boolean getLocationInfo() {
		return locationInfo;
	}

	/**
	 * Options are activated and become effective only after calling this
	 * method.
	 */
	public void activateOptions() {
		TopicConnectionFactory topicConnectionFactory;

		try {
			Context jndi;

			LogLog.debug("Getting initial context.");
			if (initialContextFactoryName != null) {
				Properties env = new Properties();
				env.put(Context.INITIAL_CONTEXT_FACTORY,
						initialContextFactoryName);
				if (providerURL != null) {
					env.put(Context.PROVIDER_URL, providerURL);
				} else {
					LogLog
							.warn("You have set InitialContextFactoryName option but not the "
									+ "ProviderURL. This is likely to cause problems.");
				}
				if (urlPkgPrefixes != null) {
					env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes);
				}

				if (securityPrincipalName != null) {
					env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName);
					if (securityCredentials != null) {
						env.put(Context.SECURITY_CREDENTIALS,
								securityCredentials);
					} else {
						LogLog
								.warn("You have set SecurityPrincipalName option but not the "
										+ "SecurityCredentials. This is likely to cause problems.");
					}
				}
				jndi = new InitialContext(env);
			} else {
				jndi = new InitialContext();
			}

			LogLog.debug("Looking up [" + tcfBindingName + "]");
			topicConnectionFactory = (TopicConnectionFactory) lookup(jndi,
					tcfBindingName);
			LogLog.debug("About to create TopicConnection.");
			if (userName != null) {
				topicConnection = topicConnectionFactory.createTopicConnection(
						userName, password);
			} else {
				topicConnection = topicConnectionFactory
						.createTopicConnection();
			}

			LogLog.debug("Creating TopicSession, non-transactional, "
					+ "in AUTO_ACKNOWLEDGE mode.");
			topicSession = topicConnection.createTopicSession(false,
					Session.AUTO_ACKNOWLEDGE);

			LogLog.debug("Looking up topic name [" + topicBindingName + "].");
			Topic topic = (Topic) lookup(jndi, topicBindingName);

			LogLog.debug("Creating TopicPublisher.");
			topicPublisher = topicSession.createPublisher(topic);

			LogLog.debug("Starting TopicConnection.");
			topicConnection.start();

			jndi.close();
		} catch (Exception e) {
			errorHandler.error(
					"Error while activating options for appender named ["
							+ name + "].", e, ErrorCode.GENERIC_FAILURE);
		}
	}

	protected Object lookup(Context ctx, String name) throws NamingException {
		try {
			return ctx.lookup(name);
		} catch (NameNotFoundException e) {
			LogLog.error("Could not find name [" + name + "].");
			throw e;
		}
	}

	protected boolean checkEntryConditions() {
		String fail = null;

		if (this.topicConnection == null) {
			fail = "No TopicConnection";
		} else if (this.topicSession == null) {
			fail = "No TopicSession";
		} else if (this.topicPublisher == null) {
			fail = "No TopicPublisher";
		}

		if (fail != null) {
			errorHandler.error(fail + " for JMSAppender named [" + name + "].");
			return false;
		} else {
			return true;
		}
	}

	/**
	 * Close this JMSAppender. Closing releases all resources used by the
	 * appender. A closed appender cannot be re-opened.
	 */
	public synchronized void close() {
		// The synchronized modifier avoids concurrent append and close
		// operations

		if (this.closed)
			return;

		LogLog.debug("Closing appender [" + name + "].");
		this.closed = true;

		try {
			if (topicSession != null)
				topicSession.close();
			if (topicConnection != null)
				topicConnection.close();
		} catch (Exception e) {
			LogLog.error("Error while closing JMSAppender [" + name + "].", e);
		}
		// Help garbage collection
		topicPublisher = null;
		topicSession = null;
		topicConnection = null;
	}

	/**
	 * This method called by {@link AppenderSkeleton#doAppend} method to do most
	 * of the real appending work.
	 */
	public void append(LoggingEvent event) {
		if (!checkEntryConditions()) {
			return;
		}

		try {
			ObjectMessage msg = topicSession.createObjectMessage();
			if (locationInfo) {
				event.getLocationInformation();
			}
			msg.setObject(event);
			topicPublisher.publish(msg);
		} catch (Exception e) {
			errorHandler.error("Could not publish message in JMSAppender ["
					+ name + "].", e, ErrorCode.GENERIC_FAILURE);
		}
	}

	/**
	 * Returns the value of the <b>InitialContextFactoryName</b> option. See
	 * {@link #setInitialContextFactoryName} for more details on the meaning of
	 * this option.
	 * */
	public String getInitialContextFactoryName() {
		return initialContextFactoryName;
	}

	/**
	 * Setting the <b>InitialContextFactoryName</b> method will cause this
	 * <code>JMSAppender</code> instance to use the
	 * {@link InitialContext#InitialContext(Hashtable)} method instead of the
	 * no-argument constructor. If you set this option, you should also at least
	 * set the <b>ProviderURL</b> option.
	 * 
	 * <p>
	 * See also {@link #setProviderURL(String)}.
	 * */
	public void setInitialContextFactoryName(String initialContextFactoryName) {
		this.initialContextFactoryName = initialContextFactoryName;
	}

	public String getProviderURL() {
		return providerURL;
	}

	public void setProviderURL(String providerURL) {
		this.providerURL = providerURL;
	}

	String getURLPkgPrefixes() {
		return urlPkgPrefixes;
	}

	public void setURLPkgPrefixes(String urlPkgPrefixes) {
		this.urlPkgPrefixes = urlPkgPrefixes;
	}

	public String getSecurityCredentials() {
		return securityCredentials;
	}

	public void setSecurityCredentials(String securityCredentials) {
		this.securityCredentials = securityCredentials;
	}

	public String getSecurityPrincipalName() {
		return securityPrincipalName;
	}

	public void setSecurityPrincipalName(String securityPrincipalName) {
		this.securityPrincipalName = securityPrincipalName;
	}

	public String getUserName() {
		return userName;
	}

	/**
	 * The user name to use when
	 * {@link TopicConnectionFactory#createTopicConnection(String, String)
	 * creating a topic session}. If you set this option, you should also set
	 * the <b>Password</b> option. See {@link #setPassword(String)}.
	 * */
	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getPassword() {
		return password;
	}

	/**
	 * The paswword to use when creating a topic session.
	 */
	public void setPassword(String password) {
		this.password = password;
	}

	/**
	 * If true, the information sent to the remote subscriber will include
	 * caller's location information. By default no location information is sent
	 * to the subscriber.
	 */
	public void setLocationInfo(boolean locationInfo) {
		this.locationInfo = locationInfo;
	}

	/**
	 * Returns the TopicConnection used for this appender. Only valid after
	 * activateOptions() method has been invoked.
	 */
	protected TopicConnection getTopicConnection() {
		return topicConnection;
	}

	/**
	 * Returns the TopicSession used for this appender. Only valid after
	 * activateOptions() method has been invoked.
	 */
	protected TopicSession getTopicSession() {
		return topicSession;
	}

	/**
	 * Returns the TopicPublisher used for this appender. Only valid after
	 * activateOptions() method has been invoked.
	 */
	protected TopicPublisher getTopicPublisher() {
		return topicPublisher;
	}

	/**
	 * The JMSAppender sends serialized events and consequently does not require
	 * a layout.
	 */
	public boolean requiresLayout() {
		return false;
	}
}
